Google CalendarのUIを真似て週表示Calendar UIを作る:本家風実装
週表示のUIを真似てみる
DOMをにらめっこしながら実装してみたもの
DOM構造調査
表:div.pbeTDb.YoVtqb.eh5oYe[role="grid"] flex
日付・曜日:div.BfTITd flex
スペーサー:div.UqLcs flex
中身:div.Zf8VQe flex
不明:div.R9tCRe
日付リスト:div.fhpo2c[role="row"] flex
終日のタスクリスト:div.Qotkjb[role="presentation"] flex 中身:div.RuAPDb.elYzab-DaY83b-ppHlrf.oXZ1yb.T8M5bd[role="presentation"] flex
div.uEzZIb[role="presentation"] flex
時刻目盛りのコンテナ:div.lqYlwe flex
この要素で↓をclippingしている
時刻目盛り:div.R6TFwe
スクロールする
目盛りの1単位:div.XsRa1c x複数
目盛りの文字列:span
div.mDPmMe[role="presentation"] flex
この要素で↓をclippingしている
div#c566 flex
横罫線:div.sJ9Raf x複数
不明:div.EDDeke
1日の予定リスト:div.BiKU4b[role="gridcell"] x複数
不要:div.ynRLnc
スペース確保用:div.QIYAPb
予定コンテナ:div.feMFof.A3o4Oe[role="presentation"]
予定のかたまり:div[role="button"] x複数
2024-04-07
12:39:50 予定を表示してみた
いいかんじ
https://gyazo.com/7053d021547a59bec7993fefb5aebfce
これで曜日とtimelineの罫線とが一致するようになった
https://gyazo.com/7dad3e52ea28b2a21434ca589271e219
12:02:08 まあこんなもんか
https://gyazo.com/bb63c7d4aa4900cd6bca0f2ee38fd9b2
次やること
いらないCSS propertiesを消す
1日だけ切り出したものも作る
これはアイテムを1つに絞ればいいだけだから、やらなくてもいいか
12:17:47 1日にしたもの
https://gyazo.com/3834534b842b96e16c2ba9c660817d7e
11:27:53 それっぽいのができた
https://gyazo.com/d7e01a7dd9c39708b6ace96417d13704
ここからは、いらないDOMとCSSを削っていく
11:21:18 幅はfrを使おうdisplay: gridでしか使えない単位だった defaultがflex: 0 1 autoなので、flex-basisを変えたいときはautoを別の値にすればいい
11:11:08 :hostをwidth: 100%; height: 100%にしないと、子孫要素の大きさにあわせられない
code:app.tsx
/** @jsx h */
import { h, render } from "../preact/mod.tsx";
import { Calendar } from "./Calendar.tsx";
const div = document.createElement("div");
const shadowRoot = div.attachShadow({ mode: "open" });
document.body.append(div);
// 消すときはカレンダーをクリックする
render(<Calendar onClose={() => div.remove()}/>, shadowRoot);
code:Calendar.tsx
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "../preact/mod.tsx";
import { useMemo } from "../preact/hooks.ts";
import { isToday, getWeek, addDays, getDate, startOfWeek } from "../date-fns/mod.ts";
export const Calendar = ({ onClose }) => {
const now = useMemo(() => new Date(), []);
//const dates = useMemo(() => 0,1,2,3,4,5,6.map((i) => addDays(startOfWeek(now), i)), now); return (<>
<style>{`
:host {
position: fixed;
top: 10%;
left: 10%;
margin: 0 auto;
min-width: 50vw;
min-height: 80vh;
border-radius: 4px;
}
/* div.pbeTDb.YoVtqb.eh5oYe相当 */
.timeline-wrap {
position: absolute;
inset: 0;
outline: none;
overflow: hidden;
display: flex;
flex-direction: column;
--row-header-width: 40px;
--column-header-height: 20px;
.column-header {
flex: 0 1 84px;
display: flex;
flex-direction: row;
position: relative;
overflow: hidden;
.corner {
white-space: nowrap;
flex: 0 1 var(--row-header-width);
margin-left: 1px;
}
.cell {
/* width: var(--timeline-width, 81px); */
flex: auto;
h2 {
font-weight: 400;
margin: 0;
text-align: center;
width: 100%;
}
}
}
.body {
position: relative;
display: flex;
flex-direction: row;
overflow: hidden;
align-items: stretch;
overflow-y: scroll;
scrollbar-width: none;
.header-container {
flex: 0 1 var(--row-header-width);
.header {
position: relative;
box-sizing: border-box;
margin-left: auto;
.time {
position: relative;
height: 40px;
padding-right: 8px;
text-align: right;
span {
display: block;
position: relative;
top: -6px;
font-size: 10px;
}
}
}
}
.week-container {
display: flex;
align-items: flex-start;
flex: auto;
.week {
min-width: 100%;
flex: none;
display: inline-flex;
overflow: hidden;
position: relative;
.borders {
border-top: rgb(218,220,224) 1px solid;
.border {
height: 40px;
}
.border::after {
content: "";
border-bottom: rgb(218,220,224) 1px solid;
position: absolute;
width: 100%;
margin-top: -1px;
z-index: 3;
pointer-events: none;
}
}
.timeline {
flex: auto;
border-left: rgb(218,220,224) 1px solid;
.event {
position: absolute;
top: calc(var(--start) / 24 * 100%);
height: calc(var(--duration) / 24 * 100%);
outline: none;
background-color: rgb(179, 225, 247);
border-color: rgb(129, 205, 242);
border-radius: 4px;
z-index: 5;
}
}
.timeline:first-child {
border-right: rgb(218,220,224) 1px solid;
}
}
}
}
}
`}</style>
<div className="timeline-wrap" onClick={onClose} role="grid">
<div className="column-header" role="row">
<div className="corner" />
{dates.map(
(date) => (<div className="cell" role="columnheader"><h2>{getDate(date.date)}</h2></div>)
)}
</div>
<div className="body" role="presentation">
<div className="header-container">
<div className="header">
(i) => (<div className="time"><span>{${${i + 1}.padStart(2, "0")}:00}</span></div>)
)}
</div>
</div>
<div className="week-container">
<div className="week">
<div className="borders">
</div>
{dates.map(
(date) => (<div className="timeline" role="gridcell">
{date.events.map((event) => <Event event={event} />)}
</div>)
)}
</div>
</div>
</div>
</div>
</>);
};
const Event = ({ event }) => {
return (
<div className="event" title={event.name} style={{ "--start": event.start, "--duration": event.duration }}>{event.name}</div>
);
};